package lpg.tool.templatec;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import lpg.util.DispatchWriter;
import lpg.util.Utility;

class Control
{
    Control(File templateFile, String packageName, String className, File classFile)
    {
        this.templateFile   = templateFile;
        this.templateName   = templateFile.getName();
        this.packageOpt     = packageName;
        this.className      = className;
        this.classFile      = classFile;
        this.parentControl  = null;
    }

    private Control(Control ctrl, File templateFile, String templateName)
    {
        parentControl       = ctrl;
        this.templateFile   = templateFile;
        this.templateName   = templateName;

        packageOpt          = ctrl.packageOpt;
        className           = ctrl.className;
        classFile           = ctrl.classFile;

        importList          = ctrl.importList;
        modifierOpt         = ctrl.modifierOpt;
        extendsOpt          = ctrl.extendsOpt;
        implementsList      = ctrl.implementsList;
        mainOpt             = ctrl.mainOpt;
        mainList            = ctrl.mainList;
        memberList          = ctrl.memberList;
    }

    void process()
    {
        parse();
        generate();
    }

    private void parse()
    {
        // Read template file
        StringBuilder buf = new StringBuilder();
        char[] block = new char[65536];
        FileInputStream fis = null;
        try
        {
            fis = new FileInputStream(templateFile);
            BufferedReader reader = new BufferedReader(new InputStreamReader(fis, Utility.getCharset()));

            int len;
            while ((len = reader.read(block)) != -1)
            {
                buf.append(block, 0, len);
            }

            reader.close();
            fis = null;
        }
        catch (IOException e)
        {
            throw new TemplatecException(e.toString(), this);
        }
        finally
        {
            Utility.close(fis);
        }
        int length = buf.length();
        if (length == 0 || buf.charAt(length - 1) != '\n')
        {
            buf.append('\n');
            length++;
        }
        char[] inputBuffer = new char[length];
        buf.getChars(0, length, inputBuffer, 0);

        // Parse template file
        State state = State.ACTION;
        for (int cursor,start = 0; start < length;)
        {
            char c;
            for (cursor = start; (c = inputBuffer[cursor]) != '\r' && c != '\n'; cursor++)
                ;

            line = new String(inputBuffer, start, cursor - start);
            bufferEOL(state);
            lineNo++;
            columnNo = 0;
            
            do
            {
                State prevState = state;
                state = state.parse(this);
                if (state != prevState)
                {
                    prevState.end(this, new String(buffer));
                    buffer = new StringBuilder();
                    startLineNo = lineNo;
                    emptyLine = true;
                }
            }
            while (columnNo >= 0);

            start = cursor + (c == '\r' && inputBuffer[cursor + 1] == '\n' ? 2 : 1);
        }
        if (state != State.ACTION)
        {
            throw new TemplatecException("\"" + state.getEndTag() + "\" expected.", this);
        }
        // As ACTION is the only legal ending state,
        // we don't need to invoke the bufferEOL() to output the lineNo info
        state.end(this, new String(buffer));
    }

    private void generate()
    {
        // Generate class file
        DispatchWriter out = null;
        try
        {
            out = new DispatchWriter();
            Generator generator = new Generator(this, out);
            generator.run();

            out.close();
            out = null;
        }
        finally
        {
            Utility.close(out);
        }
    }

    void bufferEOL(State state)
    {
        if (state != State.COMMENT && lineNo > 0)
        {
            if (state != State.ACTION && !emptyLine)
            {
                buffer.append(" /* ");
                buffer.append(templateName);
                buffer.append(':');
                buffer.append(lineNo);
                buffer.append(" */");
            }
            buffer.append('\n');
            emptyLine = true;
        }
    }

    void bufferAppend(String str, State state)
    {
        if (str.length() > 0 && state != State.COMMENT)
        {
            buffer.append(str);
            emptyLine = false;
        }
    }

    void parseDirective(String str)
    {
        if ("classname".equals(str.trim()))
        {
            Element element = new Element(className, State.SCRIPTLET,
                    templateName, startLineNo, lineNo, endColumnNo);
            if (members)
            {
                memberList.add(element);
            }
            else
            {
                mainList.add(element);
            }
            return;
        }

        int index = str.indexOf('=');
        if (index >= 0)
        {
            String name = str.substring(0, index).trim();
            String value = str.substring(index + 1);
            if ("import".equals(name))
            {
                getDirectiveValue(importList, value);
            }
            else if ("modifier".equals(name))
            {
                if (modifierOpt == null)
                {
                    modifierOpt = getDirectiveValue(value);
                }
            }
            else if ("extends".equals(name))
            {
                if (extendsOpt == null)
                {
                    extendsOpt = getDirectiveValue(value);
                }
            }
            else if ("implements".equals(name))
            {
                getDirectiveValue(implementsList, value);
            }
            else if ("main".equals(name))
            {
                if (mainOpt == null)
                {
                    mainOpt = getDirectiveValue(value);
                }
            }
            else if ("include".equals(name))
            {
                String fileName = getDirectiveValue(value).content;
                File nestedTemplateFile = new File(fileName);
                if (!nestedTemplateFile.isAbsolute())
                {
                    nestedTemplateFile = new File(templateFile.getParentFile(), fileName);
                }
                if (!nestedTemplateFile.exists())
                {
                    throw new TemplatecException("\"" + fileName + "\" does not exist.", this);
                }
                if (!nestedTemplateFile.isFile())
                {
                    throw new TemplatecException("\"" + fileName + "\" is not a file.", this);
                }
                Control subControl = new Control(this, nestedTemplateFile, fileName);
                subControl.parse();
                modifierOpt = subControl.modifierOpt;
                extendsOpt = subControl.extendsOpt;
                mainOpt = subControl.mainOpt;
            }
            else
            {
                throw new TemplatecException("\"" + str + "\" is an invalid directive.", this);
            }
        }
        else
        {
            throw new TemplatecException("\"" + str + "\" is an invalid directive.", this);
        }
    }

    void parseElement(String str, State state)
    {
        if (str.length() == 0)
        {
            // Use empty declaration "<%!%>" to return main section
            if (state == State.DECLARATION)
            {
                members = false;
            }
        }
        else
        {
            // Use declaration "<%!...%>" to begin member section
            if (state == State.DECLARATION)
            {
                members = true;
            }

            Element element = new Element(str, state,
                    templateName, startLineNo, lineNo, endColumnNo);
            if (members)
            {
                memberList.add(element);
            }
            else
            {
                mainList.add(element);
            }
        }
    }

    private void getDirectiveValue(List<Element> list, String str)
    {
        String[] sArray = str.split(",");
        for (String sElem : sArray)
        {
            list.add(getDirectiveValue(sElem));
        }
    }

    private Element getDirectiveValue(String str)
    {
        return new Element(str.trim(), State.DIRECTIVE,
                templateName, startLineNo, lineNo, endColumnNo);
    }

    String          line            = null;

    // Current line no
    int             lineNo          = 0;
    // Current column no. -1 indicates EOL
    int             columnNo        = 0;
    // The start line no of the current state
    int             startLineNo     = 1;
    // The end column no of the current state
    int             endColumnNo     = 0;

    private StringBuilder   buffer      = new StringBuilder();
    // Indicates whether the current line of the buffer is empty
    private boolean         emptyLine   = true;

    final File      templateFile;
    final String    templateName;
    final String    packageOpt;
    final String    className;
    final File      classFile;
    final Control   parentControl;

    List<Element>   importList      = new ArrayList<Element>();
    Element         modifierOpt     = null;
    Element         extendsOpt      = null;
    List<Element>   implementsList  = new ArrayList<Element>();
    Element         mainOpt         = null;
    List<Element>   mainList        = new ArrayList<Element>();
    boolean         members         = false;
    List<Element>   memberList      = new ArrayList<Element>();

    static class Element
    {
        Element(String content, State state, String fileName,
                int startLineNo, int endLineNo, int endColumnNo)
        {
            this.content        = content;
            this.state          = state;
            this.fileName       = fileName;
            this.startLineNo    = startLineNo;
            this.endLineNo      = endLineNo;
            this.endColumnNo    = endColumnNo;
        }

        final String content;
        final State state;
        final String fileName;
        final int startLineNo;
        final int endLineNo;
        final int endColumnNo;
    }

}
